#include <stdarg.h>
#include <stdint.h>
#include <string.h>
#include "gc.h"
#include "redintobject.h"
#include "redmapobject.h"
#include "redboolobject.h"
#include "rednullobject.h"
#include "redarrayobject.h"
#include "redfloatobject.h"
#include "redtupleobject.h"
#include "redstringobject.h"
#include "redfunctionobject.h"
#include "redcfunctionobject.h"
#include "redexceptionobject.h"
#include "redboundmethodobject.h"

static RedObject *tuple_len(RedObject *self)
{
	return RedInt_FromInt(RedTuple_Length(self));
}

static RedObject *tuple_repr(RedObject *self)
{
	if (RedTuple_Length(self) == 0)
		return RedString_FromCString("()");

	if (!RedObject_ReprEnter(self))
		return RedString_FromCString("...");

	RedObject *result = RedString_FromChar('(');

	for (int i = 0; i < RedTuple_Length(self); i++)
	{
		RedObject *item = RedObject_Repr(RedTuple_Items(self)[i]);

		if (!item)
		{
			Red_UNREF(result);
			RedObject_ReprLeave(self);
			return NULL;
		}

		RedObject *newstring = RedString_Append(result, item);

		Red_UNREF(item);
		Red_UNREF(result);

		if (i < RedTuple_Length(self) - 1)
			result = RedString_AppendCString(newstring, ", ");
		else if (RedTuple_Length(self) > 1)
			result = RedString_AppendCString(newstring, ")");
		else
			result = RedString_AppendCString(newstring, ",)");

		Red_UNREF(newstring);
	}

	RedObject_ReprLeave(self);
	return result;
}

static char tuple_init(RedObject *self, RedObject *args)
{
	if (!Red_INIT())
		return 0;

	/* args == NULL ---> internal object */
	if (!args)
		return 1;

	if (!RedTuple_Exact(args))
	{
		RedException_ThrowFormat(&RedExc_TypeError, "'tuple' expected for 'args' argument");
		return 0;
	}

	for (int i = 0; i < RedTuple_Length(args); i++)
	{
		if (RedObject_GC_IsTracked(self))
			RedObject_GC_Insert(RedTuple_Items(args)[i]);

		Red_REF(RedTuple_Items(args)[i]);
		RedTuple_Items(self)[i] = RedTuple_Items(args)[i];
	}

	RedTuple_Length(self) = RedTuple_Length(args);
	return 1;
}

static RedObject *tuple_alloc(RedTypeObject *self, RedObject *args)
{
	if (!RedTuple_Exact(args))
	{
		RedException_ThrowFormat(&RedExc_TypeError, "'tuple' expected for 'args' argument");
		return NULL;
	}

	int count = RedTuple_Length(args);
	return RedObject_CreateVar(self, args, sizeof(RedObject *) * count);
}

static void tuple_clear(RedObject *self)
{
	for (int i = 0; i < RedTuple_Length(self); i++)
		Red_UNREF(RedTuple_Items(self)[i]);
}

static void tuple_release(RedObject *self)
{
	tuple_clear(self);
	Red_RELEASE();
}

static void tuple_traverse(RedObject *self, tp_visit_fn_t visit, void *args)
{
	for (int i = 0; i < RedTuple_Length(self); i++)
		Red_VISIT(RedTuple_Items(self)[i]);
}

static RedObject *tuple_foreach(RedObject *self, RedObject *args)
{
	RedObject *callable;

	if (!RedTuple_Unpack(args, "C", &callable))
		return NULL;

	RedObject *result = RedTuple_FromLength(RedTuple_Length(self));

	for (int i = 0; i < RedTuple_Length(self); i++)
		RedTuple_Items(result)[i] = NULL;

	for (int i = 0; i < RedTuple_Length(self); i++)
	{
		RedObject *n = RedInt_FromInt(i);
		RedObject *arg = RedTuple_Items(self)[i];
		RedObject *tuple = RedTuple_FromObjects(2, n, arg);
		RedTuple_Items(result)[i] = RedObject_Invoke(callable, tuple);

		Red_UNREF(n);
		Red_UNREF(tuple);

		if (!(RedTuple_Items(result)[i]))
		{
			Red_UNREF(result);
			return NULL;
		}
	}

	return result;
}

static RedObject *tuple_contains(RedObject *self, RedObject *args)
{
	RedObject *value;

	if (!RedTuple_Unpack(args, "?", &value))
		return NULL;

	for (int i = 0; i < RedTuple_Length(self); i++)
	{
		RedObject *item = RedTuple_Items(self)[i];
		RedObject *equals = RedObject_Eq(value, item);

		if (!equals)
			return NULL;

		if (RedBool_IsTrue(equals))
		{
			Red_UNREF(equals);
			Red_Return_True();
		}

		Red_UNREF(equals);
	}

	Red_Return_False();
}

static RedMethodDesc tuple_methods[] =
{
	{ "foreach"	, tuple_foreach		},
	{ "contains", tuple_contains	},
	{ NULL, NULL },
};

RedTypeObject RedType_Tuple =
{
	RedType_INIT("tuple", RedTupleObject, RedType_Object)

	.tp_len			= tuple_len,
	.tp_str			= tuple_repr,
	.tp_repr		= tuple_repr,

	.tp_init		= tuple_init,
	.tp_alloc		= tuple_alloc,
	.tp_clear		= tuple_clear,
	.tp_release		= tuple_release,
	.tp_traverse	= tuple_traverse,

	.tp_add			= RedTuple_Concat,
	.tp_aug_add		= RedTuple_Concat,
	.tp_getitem		= RedTuple_GetItem,

	.tp_methods		= tuple_methods,
};

RedObject *RedTuple_FromLength(int length)
{
	RedObject *result = RedObject_CreateVar(&RedType_Tuple, NULL, sizeof(RedObject *) * length);

	if (!result)
		return NULL;

	RedTuple_Length(result) = length;
	return result;
}

RedObject *RedTuple_FromObjects(int length, ...)
{
	va_list args;
	RedObject *result = RedTuple_FromLength(length);

	if (!result)
		return NULL;

	va_start(args, length);

	for (int i = 0; i < length; i++)
	{
		RedTuple_Items(result)[i] = va_arg(args, RedObject *);
		Red_REF(RedTuple_Items(result)[i]);
	}

	va_end(args);
	return result;
}

RedObject *RedTuple_Append(RedObject *self, RedObject *item)
{
	if (!RedTuple_Exact(self))
	{
		RedException_ThrowFormat(&RedExc_TypeError, "'tuple' expected for 'self' argument");
		return NULL;
	}

	RedObject *result = RedTuple_FromLength(RedTuple_Length(self) + 1);

	for (int i = 0; i < RedTuple_Length(self); i++)
	{
		Red_REF(RedTuple_Items(self)[i]);
		RedTuple_Items(result)[i] = RedTuple_Items(self)[i];
	}

	if (RedObject_GC_IsTracked(self))
	{
		RedObject_GC_Insert(item);
		RedObject_GC_Insert(result);
	}

	Red_REF(item);
	RedTuple_Items(result)[RedTuple_Length(self)] = item;
	return result;
}

RedObject *RedTuple_Concat(RedObject *self, RedObject *other)
{
	if (!RedTuple_Exact(self))
	{
		RedException_ThrowFormat(&RedExc_TypeError, "'tuple' expected for 'self' argument");
		return NULL;
	}

	if (!RedTuple_Exact(other))
	{
		RedException_ThrowFormat(&RedExc_TypeError, "'tuple' expected for 'other' argument");
		return NULL;
	}

	RedObject *result = RedTuple_FromLength(RedTuple_Length(self) + RedTuple_Length(other));

	for (int i = 0; i < RedTuple_Length(self); i++)
	{
		if (RedObject_GC_IsTracked(other))
			if (!RedObject_GC_IsTracked(self))
				RedObject_GC_Insert(RedTuple_Items(self)[i]);

		Red_REF(RedTuple_Items(self)[i]);
		RedTuple_Items(result)[i] = RedTuple_Items(self)[i];
	}

	for (int i = 0; i < RedTuple_Length(other); i++)
	{
		if (RedObject_GC_IsTracked(self))
			if (!RedObject_GC_IsTracked(other))
				RedObject_GC_Insert(RedTuple_Items(other)[i]);

		Red_REF(RedTuple_Items(other)[i]);
		RedTuple_Items(result)[i + RedTuple_Length(self)] = RedTuple_Items(other)[i];
	}

	if (RedObject_GC_IsTracked(self) || RedObject_GC_IsTracked(other))
		RedObject_GC_Insert(result);

	return result;
}

RedObject *RedTuple_GetItem(RedObject *self, RedObject *index)
{
	if (!RedInt_Exact(index))
	{
		RedException_ThrowFormat(&RedExc_TypeError, "'int' expected for 'index' argument");
		return NULL;
	}

	if (!RedTuple_Exact(self))
	{
		RedException_ThrowFormat(&RedExc_TypeError, "'tuple' expected for 'self' argument");
		return NULL;
	}

	int pos = (int)RedInt_Value(index);

	if (pos < 0 || pos >= RedTuple_Length(self))
	{
		RedException_ThrowFormat(&RedExc_IndexError, "Index %d out of range", pos);
		return NULL;
	}

	Red_REF(RedTuple_Items(self)[pos]);
	return RedTuple_Items(self)[pos];
}

char RedTuple_Unpack(RedObject *self, const char *format, ...)
{
	if (!RedTuple_Exact(self))
	{
		RedException_ThrowFormat(&RedExc_TypeError, "'tuple' expected for arguments pack");
		return 0;
	}

	int count = 0;
	char varg = 0;
	const char *s = NULL;

	for (s = format; *s && !varg; s++)
	{
		switch (*s)
		{
			/* variable argument */
			case '>':
			{
				varg = 1;
				break;
			}

			/* null */
			case 'n':
				break;

			case '?': /* anything */
			case 'a': /* array */
			case 'c': /* class */
			case 'C': /* callable */
			case 'e': /* exception */
			case 'F': /* function */
			case 'm': /* map */
			case 'M': /* method */
			case 'o': /* object */
			case 't': /* tuple */
			case 'T': /* typed object */
			{
				count++;
				break;
			}

			case 'b': /* bool */
			case 'f': /* float */
			case 'i': /* int */
			{
				switch (*++s)
				{
					case '*': /* convert to C-style */
					case '#': /* keep in object style */
						break;

					default:
					{
						s--;
						break;
					}
				}

				count++;
				break;
			}

			/* string */
			case 's':
			{
				switch (*++s)
				{
					case '*': /* convert to C-style */
					case '#': /* keep in object style */
					case '@': /* convert to C-style and length */
						break;

					default:
					{
						s--;
						break;
					}
				}

				count++;
				break;
			}

			default:
			{
				RedException_ThrowFormat(&RedExc_FormatError, "Unrecognized tuple format '%c'", *s);
				break;
			}
		}
	}

	if (*s)
	{
		RedException_ThrowFormat(&RedExc_FormatError, "Character '%c' after variable-argument directive", *s);
		return 0;
	}

	if (varg)
	{
		if (RedTuple_Length(self) < count)
		{
			RedException_ThrowFormat(&RedExc_ArgumentError, "Function takes at least %d arguments, but %d given", count, RedTuple_Length(self));
			return 0;
		}
	}
	else
	{
		if (!count && RedTuple_Length(self))
		{
			RedException_ThrowFormat(&RedExc_ArgumentError, "Function takes no arguments, but %d given", RedTuple_Length(self));
			return 0;
		}
		else if (RedTuple_Length(self) != count)
		{
			RedException_ThrowFormat(&RedExc_ArgumentError, "Function takes exactly %d arguments, but %d given", count, RedTuple_Length(self));
			return 0;
		}
	}

	int n = 0;
	char ok = 1;
	va_list args;
	va_start(args, format);

#define next_arg()	RedTuple_Items(self)[n++]
#define fetch_arg(type)																									\
	{																													\
		RedObject *item = next_arg();																					\
																														\
		if (!Red##type##_Exact(item))																					\
		{																												\
			ok = 0;																										\
			RedException_ThrowFormat(&RedExc_TypeError, "'%s' expected for %d-th argument", RedType_##type.tp_name, n);	\
			break;																										\
		}																												\
																														\
		*va_arg(args, RedObject **) = item;																				\
		break;																											\
	}

	for (s = format; *s && ok; s++)
	{
		switch (*s)
		{
			case 'a': fetch_arg(Array)			/* array */
			case 'c': fetch_arg(Type)			/* class */
			case 'F': fetch_arg(Function)		/* function */
			case 'm': fetch_arg(Map)			/* map */
			case 'M': fetch_arg(BoundMethod)	/* method */
			case 'o': fetch_arg(Object)			/* object */
			case 't': fetch_arg(Tuple)			/* tuple */

			/* anything */
			case '?':
			{
				*va_arg(args, RedObject **) = next_arg();
				break;
			}

			/* typed object */
			case 'T':
			{
				RedObject *object = next_arg();
				RedTypeObject *type = va_arg(args, RedTypeObject *);

				if (object->ob_type == type)
				{
					*va_arg(args, RedObject **) = object;
					break;
				}

				ok = 0;
				RedException_ThrowFormat(&RedExc_TypeError, "`%s` object expected for %d-th argument", type->tp_name, n);
				break;
			}

			/* null */
			case 'n':
			{
				RedObject *item = next_arg();

				/* null object is not needed to be stored */
				if (RedNull_IsNull(item))
					break;

				ok = 0;
				RedException_ThrowFormat(&RedExc_TypeError, "Null object expected for %d-th argument", n);
				break;
			}

			/* exception */
			case 'e':
			{
				RedObject *item = next_arg();

				if (!RedType_IsInstance(item, &RedExc_Exception))
				{
					ok = 0;
					RedException_ThrowFormat(&RedExc_TypeError, "Exception object expected for %d-th argument", n);
					break;
				}

				*va_arg(args, RedObject **) = item;
				break;
			}

			/* variable-argument */
			case '>':
			{
				RedObject *tuple = RedTuple_FromLength(RedTuple_Length(self) - n);

				for (int i = 0; n < RedTuple_Length(self); i++, n++)
				{
					Red_REF(RedTuple_Items(self)[n]);
					RedTuple_Items(tuple)[i] = RedTuple_Items(self)[n];
				}

				*va_arg(args, RedObject **) = tuple;
				break;
			}

			/* callable */
			case 'C':
			{
				RedObject *item = next_arg();																					

				if (!RedObject_Callable(item))
				{
					ok = 0;
					RedException_ThrowFormat(&RedExc_TypeError, "'%s' object of %d-th argument is not callable", RedObject_ClassName(item), n);
					break;
				}

				*va_arg(args, RedObject **) = item;
				break;
			}

			/* bool */
			case 'b':
			{
				switch (*++s)
				{
					/* convert to C-style */
					case '*':
					{
						RedObject *item = next_arg();

						if (RedBool_IsTrue(item))
							*va_arg(args, char *) = 1;
						else
							*va_arg(args, char *) = 0;

						break;
					}

					/* keep in object style */
					default: s--;
					case '#':
					{
						RedObject *item = next_arg();

						if (RedBool_IsTrue(item))
							*va_arg(args, RedObject **) = Red_True;
						else
							*va_arg(args, RedObject **) = Red_False;

						break;
					}
				}

				break;
			}

			/* int */
			case 'i':
			{
				switch (*++s)
				{
					/* keep in object style */
					default:	s--;
					case '#':	fetch_arg(Int)

					/* convert to C-style */
					case '*':
					{
						RedObject *item = next_arg();

						if (!RedInt_Exact(item))
						{
							ok = 0;
							RedException_ThrowFormat(&RedExc_TypeError, "'int' expected for %d-th argument", n);
							break;
						}

						*va_arg(args, int64_t *) = RedInt_Value(item);
						break;
					}
				}
				
				break;
			}

			/* float */
			case 'f':
			{
				switch (*++s)
				{
					/* keep in object style */
					default:	s--;
					case '#':	fetch_arg(Float)

					/* convert to C-style */
					case '*':
					{
						RedObject *item = next_arg();

						if (!RedFloat_Exact(item))
						{
							ok = 0;
							RedException_ThrowFormat(&RedExc_TypeError, "'float' expected for %d-th argument", n);
							break;
						}

						*va_arg(args, float *) = RedFloat_Value(item);
						break;
					}
				}

				break;
			}

			/* string */
			case 's':
			{
				switch (*++s)
				{
					/* keep in object style */
					default:	s--;
					case '#':	fetch_arg(String)

					/* convert to C-style */
					case '*':
					{
						RedObject *item = next_arg();

						if (!RedString_Exact(item))
						{
							ok = 0;
							RedException_ThrowFormat(&RedExc_TypeError, "'string' expected for %d-th argument", n);
							break;
						}

						*va_arg(args, char **) = RedString_Buffer(item);
						break;
					}

					/* convert to C-style and length */
					case '@':
					{
						RedObject *item = next_arg();

						if (!RedString_Exact(item))
						{
							ok = 0;
							RedException_ThrowFormat(&RedExc_TypeError, "'string' expected for %d-th argument", n);
							break;
						}

						*va_arg(args, int *) = RedString_Length(item);
						*va_arg(args, char **) = RedString_Buffer(item);
						break;
					}
				}
				
				break;
			}
		}
	}

#undef next_arg
#undef fetch_arg

	va_end(args);
	return ok;
}
